﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Windows;
using System.Drawing;
using System.Diagnostics;
using Silvermoon.Animations;
using Silvermoon.Controls.Base;
using Silvermoon.Utils;
using Silvermoon.UI;
using System.Linq;


namespace Silvermoon.Controls
{
    /// <summary>
    /// Control that scrolls other controls horizontally.
    /// </summary>
    public class Pivot : NavigationBase, ISynchronizable
    {
        delegate void TileLayout();

        #region fields

        private Color unselectedTextColor = PhoneColors.Unfocused;
        private PivotMode mode = PivotMode.Transparency;
        private float tileSizeFactor = 1f;
        private double power = 2d;
        protected List<Tile> tiles = new List<Tile>();
        private float scale;
        private TileLayout tileLayout;
        private int startOffset;
        private int tabSpace = 20;


        #endregion
        #region ctors

        public Pivot()
        {
            tileLayout = LayoutCentered;
        }

        protected override void Dispose(bool dispose)
        {
            DisposeTiles(true);
            tiles = null;
            base.Dispose(dispose);
        }

        private void DisposeTiles(bool complete)
        {
            if (tiles != null)
            {
                foreach (Tile tile in tiles)
                {
                    if (!complete) tile.Control = null;
                    tile.Dispose();
                }
            }
        }

        #endregion
        #region accessors

        public static PropertyAccessor<Pivot, float> TileSizeFactorProperty = PropertyAccessor.Register<Pivot, float>("TIleSizeFactor", (c) => (c).tileSizeFactor, (c, v) => (c).TileSizeFactor = v);

        public static PropertyAccessor<Pivot, float> PowerAccessor = PropertyAccessor.Register<Pivot, float>("Power", (c) => (float)(c).power, (c, v) => (c).Power = v);

        #endregion
        #region props

        /// <summary>
        /// Gets or sets the space between two tabs. this is also the double value of the space between the left border and the first tab.
        /// </summary>
        public int TabSpace
        {
            get { return tabSpace; }
            set
            {
                if (tabSpace != value)
                {
                    tabSpace = value;
                    OnTabSpaceChanged();
                }
            }
        }

        public int StartOffset
        {
            get { return startOffset; }
            set
            {
                if (startOffset != value)
                {
                    startOffset = value;
                    OnStartOffsetChanged();
                }
            }
        }

        public Color UnselectedTextColor
        {
            get { return unselectedTextColor; }
            set
            {
                if (unselectedTextColor != value)
                {
                    unselectedTextColor = value;
                    Invalidate(InvalidationFlag.Appearance);
                }
            }
        }


        public bool IsCentered
        {
            get { return (mode & PivotMode.Tabbed) == 0; }
            set
            {
                if (IsCentered != value)
                {
                    Mode = !value ? mode | PivotMode.Tabbed : mode & ~PivotMode.Tabbed;
                }
            }
        }

        public bool IsTabbed
        {
            get { return (mode & PivotMode.Tabbed) != 0; }
            set
            {
                if (IsTabbed != value)
                {
                    Mode = value ? mode | PivotMode.Tabbed : mode & ~PivotMode.Tabbed;
                }
            }
        }

        public bool IsScaled
        {
            get { return (mode & PivotMode.Scale) != 0; }
        }

        public bool IsTransparent
        {
            get { return (mode & PivotMode.Transparency) != 0; }
        }

        /// <summary>
        /// Gets or sets what is affected by the <see cref="P:Power"/> property.
        /// </summary>
        public PivotMode Mode
        {
            get { return mode; }
            set
            {
                if (mode != value)
                {
                    mode = value;
                    OnModeChanged();
                }
            }
        }


        /// <summary>
        /// Gets or sets the relative size of a tile in order of the Pivot.
        /// </summary>
        public float TileSizeFactor
        {
            get { return tileSizeFactor; }
            set
            {
                if (value <= 0f) throw new ArgumentOutOfRangeException("TileSizeFactor");
                if (tileSizeFactor != value)
                {
                    tileSizeFactor = value;
                    Invalidate(InvalidationFlag.Appearance | InvalidationFlag.Width | InvalidationFlag.Height);
                }
            }
        }

        /// <summary>
        /// Gets or sets the relation between neigboured tiles with the formula 1 : 1 / (2^Power).
        /// If Mode.Scale is set to true, this affects the size of the tile, if Mode.Blend is set to true, this affects the Opacity.
        /// </summary>
        public double Power
        {
            get { return power; }
            set
            {
                if (power != value)
                {
                    power = value;
                    Invalidate(InvalidationFlag.Appearance | InvalidationFlag.Width | InvalidationFlag.Height);
                }
            }
        }


        /// <summary>
        /// Gets the width of a tile.
        /// </summary>
        protected float TileWidth
        {
            get { return (Width / tileSizeFactor); }
        }

        /// <summary>
        /// Gets the number of always visible tiles.
        /// </summary>
        protected int NumTiles
        {
            get;
            private set;
        }

        #endregion
        #region methods


        protected virtual void OnModeChanged()
        {
            if ((mode & PivotMode.Tabbed) != 0)
            {
                tileLayout = LayoutTabbed;
            }
            else
            {
                tileLayout = LayoutCentered;
            }
            Invalidate(InvalidationFlag.Appearance);
        }


        protected virtual void OnStartOffsetChanged()
        {
            Invalidate(InvalidationFlag.Layout);
        }


        protected override void OnLayout()
        {
            base.OnLayout();
            if (CheckFlag(InvalidationFlag.Width))
            {
                scale = 0.5f + CalculateScaleFactor(1d) / 2f; // CalculateFactor(0d)+CalculateFactor(1d)/2f;
                if (!IsTabbed)
                {
                    NumTiles = CalculateNumTiles();
                    RebuildTiles();
                }
            }
            if (CheckFlag(InvalidationFlag.Size | InvalidationFlag.Appearance | InvalidationFlag.Layout))
            {
                LayoutTiles();
            }
        }

        protected virtual int CalculateNumTiles()
        {
            if (!IsTabbed)
            {
                int result = 1;
                double total = 1f - CalculateScaleFactor(0d) / tileSizeFactor - 0.000001f;
                float p = 1f;
                while (total > 0f)
                {
                    total -= 2f * CalculateScaleFactor(p) / tileSizeFactor;
                    p += 1f;
                    result += 2;
                    if (result > 16) break;
                }
                return result;
            }
            else return tiles.Count;
        }

        protected virtual void RebuildTiles()
        {
            int numTiles = NumTiles + 2;
            while (tiles.Count < numTiles)
            {
                Tile tile = new Tile();
                tiles.Add(tile);
                VisibleControls.Add(tile);
            }
            while (tiles.Count > numTiles)
            {
                int n = tiles.Count - 1;
                Tile tile = tiles[n];
                tiles.RemoveAt(n);
                tile.Control = null;
                VisibleControls.Remove(tile);
            }

            Size size = new Size((int)TileWidth, Height);
            foreach (Tile tile in tiles)
            {
                tile.Size = size;
            }
        }

        protected virtual void LayoutTiles()
        {
            Rectangle client = ClientBounds;
            tileLayout();
            containerGenerator.ReleaseUnusedContainers();
        }

        private Tile GetTileByIndex(int index, int delta)
        {
            if (tiles.Count <= index)
            {
                Tile tile = new Tile();
                tile.Delta = delta;
                tiles.Add(tile);
                VisibleControls.Add(tile);
                return tile;
            }
            else
            {
                Tile tile = tiles[index];
                if (tile.Parent == null) VisibleControls.Add(tile);
                tile.ScaleFactor = 1f;
                return tile;
            }
        }


        /// <summary>
        /// Layouts the tiles in tabbed mode.
        /// </summary>
        private void LayoutTabbed()
        {
            VisibleControls.Clear();
            INavigationNode selectedNode = SelectedNode;
            INavigationNode measureNode = this.offset <= 0f ? selectedNode : selectedNode.Previous;

            int offset = 0;
            if (this.offset != 0f)
            {
                Control c = containerGenerator.GetContainerFromData(measureNode);
                int w0 = c != null ? c.Width + tabSpace : 0;
                offset = (int)(this.offset * w0);
            }

            int left = offset + startOffset + tabSpace;
            int width = this.Width;
            int height = this.Height;
            int index = 0;
            int right = left;
            INavigationNode node = selectedNode;

            foreach (var t in tiles)
            {
                if (t.Control != null) t.Control.Parent = null;
                t.Control = null;
            }

            node = selectedNode != null ? selectedNode.Previous : null;
            INavigationNode endNode = node;
            INavigationNode prev = null;
            int delta = 0;
            while (right > tabSpace && node != null)
            {
                Tile tile = GetTileByIndex(index, --delta);
                tile.Opacity = 255;
                tile.TextColor = unselectedTextColor;
                Control control = containerGenerator.GetContainerFromData(node);
                tile.Control = control;

                InitializeControl(control);
                tile.Node = node;
                if (node == selectedNode) tile.TextColor = Color.Empty;
                int w = control.Width;
                tile.Bounds = new Rectangle(right - w - tabSpace, 0, w, height);
                right -= w + tabSpace;
                node = node.Previous;
                if (endNode == node) break;
                index++;
                prev = selectedNode.Previous;
            }

            node = selectedNode;

            endNode = node;
            if (offset < 0f) endNode = null;

            delta = 0;
            while (left < Width && node != null)
            {
                bool isUsed = node == prev;
                if (isUsed && offset == 0f) break;

                if (offset < 0f) prev = selectedNode;
                Tile tile = GetTileByIndex(index, delta++);
                tile.TextColor = unselectedTextColor;
                Control control = containerGenerator.GetContainerFromData(node);
                tile.Opacity = GetTileOpacity(isUsed);
                tile.Control = control;
                InitializeControl(control);
                tile.Node = node;
                if (node == selectedNode) tile.TextColor = Color.Empty;
                tile.Bounds = new Rectangle(left, 0, control.Width, height);
                if (isUsed) break;
                node = node.Next;
                if (node == endNode) break;
                left += control.Width + tabSpace;
                index++;
            }
        }

        private int GetTileOpacity(bool isUsed)
        {
            if (!isUsed) return 255;
            float v = Math.Min(1f, Math.Abs(offset));
            if (offset > 0) v = 1f - v;
            return (int)(255f * v);
        }


        private void SetTileWidth(Tile tile)
        {
            Control c = tile.Control;
            tile.Width = c != null ? c.Width : (int)TileWidth;
        }

        /// <summary>
        /// Layouts the tiles in centered mode.
        /// </summary>
        protected virtual void LayoutCentered()
        {
            INavigationNode middleNode = GetFocusedNode();

            float offset = MathUtil.FractionalDigits(this.offset);
            bool blend = (mode & PivotMode.Transparency) != 0;

            float tileWidth = TileWidth;
            offset *= scale;
            bool scaleMode = (mode & PivotMode.Scale) != 0;
            float factor = CalculateScaleFactor(offset);
            float opacity = scaleMode ? factor : CalculateOpacityFactor(offset);
            int index = tiles.Count - 1;
            Tile tile = tiles[index--];
            tile.Delta = 0;
            Control control = containerGenerator.GetContainerFromData(middleNode); ;
            tile.Control = control;
            InitializeControl(control);
            tile.Node = middleNode;
            tile.Visible = tile.Control != null;
            if (tile.Visible)
            {
                tile.Color = Color.Empty;
                tile.Control.Invalidate(InvalidationFlag.Color | InvalidationFlag.Appearance);
            }

            float x = (float)(offset * tileWidth) + Width / 2;
            float w = tileWidth * factor;
            float tw2 = tileWidth / 2f;
            tile.X = (int)(x - tw2);
            tile.ScaleFactor = factor;
            tile.Opacity = blend ? (int)(255f * opacity) : 255;
            float xr = x - w / 2f;
            x += w / 2f;

            int max = (NumTiles / 2) + 1;
            INavigationNode next = middleNode;
            INavigationNode prev = middleNode;
            int delta = 0;
            for (int i = 1; i <= max; i++)
            {
                delta++;
                next = next != null ? next.Next : null;
                tile = tiles[index--];
                tile.Delta = delta;
                tile.Node = next;

                tile.Color = unselectedTextColor;
                float offs = offset + i * scale;
                factor = CalculateScaleFactor(offs);
                opacity = scaleMode ? factor : CalculateOpacityFactor(offs);
                w = tileWidth * factor;
                tile.X = (int)(x - tw2 + w / 2f);
                tile.ScaleFactor = factor;
                tile.Opacity = blend ? (int)(255f * opacity) : 255;
                x += w;
                bool visible = ClientBounds.IntersectsWith(tile.Bounds);
                if (visible)
                {
                    Control c = containerGenerator.GetContainerFromData(next);
                    tile.Control = c;
                    InitializeControl(c);
                    visible = c != null;
                }
                else tile.Control = null;

                tile.Visible = visible;

                prev = prev != null ? prev.Previous : null;
                tile = tiles[index--];
                tile.Delta = -delta;
                tile.Node = prev;

                tile.Color = unselectedTextColor;
                offs = offset - i * scale;
                factor = CalculateScaleFactor(offs);
                opacity = scaleMode ? factor : CalculateOpacityFactor(offs);
                w = tileWidth * factor;
                xr -= w;
                tile.X = (int)(xr - tw2 + w / 2f);
                tile.ScaleFactor = factor;
                tile.Opacity = blend ? (int)(255f * opacity) : 255;
                visible = ClientBounds.IntersectsWith(tile.Bounds);
                if (visible)
                {
                    Control c = containerGenerator.GetContainerFromData(prev);
                    tile.Control = c;
                    InitializeControl(c);
                    visible = c != null;
                }
                else tile.Control = null;
                tile.Visible = visible;
            }
            Size tileSize = new Size((int)TileWidth, Height);
            foreach (Tile t in tiles)
            {
                if (t.Visible) t.Size = tileSize;
            }
        }


        /// <summary>
        /// Gets the bounds for a given tile.
        /// </summary>
        /// <param name="index">The index of the tile relative to current. 0 means current, -1 means previous, 1 means next, 2 means next over next, and so one.</param>
        /// <returns>Bounds for tile.</returns>
        protected Rectangle GetTileBounds(int index, float offset)
        {
            float factor = CalculateScaleFactor(offset + (float)index);
            float tw = factor * TileWidth;

            //int tw = TileWidth;
            int left = (int)((Width - tw) / 2f + (tw * offset));
            int right = (int)(left + tw);
            int top = 0;

            left += (int)(index * tw);

            return new Rectangle(left, top, (int)tw, Height);
        }

        private float CalculateScaleFactor(double offset)
        {
            return ((mode & PivotMode.Scale) == 0) ? (float)(1d / Math.Pow(1d, Math.Abs(offset))) : (float)(1d / Math.Pow(power, Math.Abs(offset)));
        }

        private float CalculateOpacityFactor(double offset)
        {
            return (float)(1d / Math.Pow(power, Math.Abs(offset)));
        }

        protected internal override void OnEndPan(EventArgs e)
        {
            base.OnEndPan(e);
        }


        protected virtual void OnTabSpaceChanged()
        {
            Invalidate(InvalidationFlag.Layout);
        }


        #endregion
    }
}
